using System;
using System.Collections.Generic;

namespace Kangaroo.Deferreds
{
    internal class Deferred<T> : IDeferred<T>
    {
        private readonly List<Action<T>> _doneCallbacks = new List<Action<T>>();
        private readonly List<Action<T>> _doneCallbacksRegisterdAfter = new List<Action<T>>();
        private readonly List<Action<Exception>> _failCallbacks = new List<Action<Exception>>();
        private readonly List<Action<Exception>> _failCallbacksRegisterdAfter = new List<Action<Exception>>();
        // TODO: Could be improved by using ReadWriteLockerSlim
        private readonly object _stateSyncRoot = new object();
        private Exception _exception;
        private T _result;
        private volatile DeferredState _state;

        #region IDeferred<T> Members

        public DeferredState State
        {
            get { return _state; }
            private set
            {
                lock(_stateSyncRoot)
                {
                    if(_state != value)
                    {
                        _state = value;
                        OnStateChanged();
                    }
                }
            }
        }

        public IDeferred<T> OnDone(params Action<T>[] doneCallbacks)
        {
            lock(_stateSyncRoot)
            {
                switch(_state)
                {
                    case DeferredState.Pending:
                        lock(_doneCallbacks)
                        {
                            _doneCallbacks.AddRange(doneCallbacks);
                        }
                        break;
                    case DeferredState.Resolved:
                        InvokeCallbacks(doneCallbacks, _result);
                        lock(_doneCallbacksRegisterdAfter)
                        {
                            _doneCallbacksRegisterdAfter.AddRange(doneCallbacks);
                        }
                        break;
                    case DeferredState.Rejected:
                        lock(_doneCallbacksRegisterdAfter)
                        {
                            _doneCallbacksRegisterdAfter.AddRange(doneCallbacks);
                        }
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
            return this;
        }

        public void Resolve(T result)
        {
            lock(_stateSyncRoot)
            {
                switch(_state)
                {
                    case DeferredState.Pending:
                        _result = result;
                        State = DeferredState.Resolved;
                        break;
                    case DeferredState.Resolved:
                        break;
                    case DeferredState.Rejected:
                        throw new InvalidOperationException("Cannot resolve a deferred that is already rejected.");
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }

        public IDeferred<T> OnFail(params Action<Exception>[] failCallbacks)
        {
            lock(_stateSyncRoot)
            {
                switch(_state)
                {
                    case DeferredState.Pending:
                        lock(_failCallbacks)
                        {
                            _failCallbacks.AddRange(failCallbacks);
                        }
                        break;
                    case DeferredState.Resolved:
                        lock(_failCallbacksRegisterdAfter)
                        {
                            _failCallbacksRegisterdAfter.AddRange(failCallbacks);
                        }
                        break;
                    case DeferredState.Rejected:
                        InvokeCallbacks(failCallbacks, _exception);
                        lock(_failCallbacksRegisterdAfter)
                        {
                            _failCallbacksRegisterdAfter.AddRange(failCallbacks);
                        }
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
            return this;
        }

        public void Reject(Exception exception)
        {
            lock(_stateSyncRoot)
            {
                switch(_state)
                {
                    case DeferredState.Pending:
                        _exception = exception;
                        State = DeferredState.Rejected;
                        break;
                    case DeferredState.Resolved:
                        _exception = exception;
                        State = DeferredState.Rejected;
                        break;
                    case DeferredState.Rejected:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }

        public IDeferred<T> Then(Action<T> doneCallback, Action<Exception> failCallback = null)
        {
            return failCallback == null ? OnDone(doneCallback) : OnDone(doneCallback).OnFail(failCallback);
        }

        public IDeferred<TResult> Pipe<TResult>(
            Func<T, TResult> doneTransform,
            Func<Exception, Exception> failTransform = null)
        {
            if(doneTransform == null)
                throw new ArgumentNullException("doneTransform");
            return Deferred.New<TResult>(deferred =>
            {
                OnDone(result => deferred.Resolve(doneTransform(result)));
                OnFail(exception => failTransform(exception));
            });
        }

        public IDeferred<TResult> Chain<TResult>(
            Func<T, IDeferred<TResult>> doneChain,
            Func<Exception, IDeferred<TResult>> failChain = null)
        {
            if(doneChain == null)
                throw new ArgumentNullException("doneChain");
            return Deferred.New<TResult>(deferred =>
            {
                OnDone(result1 => doneChain(result1).OnDone(deferred.Resolve));
                OnFail(exception1 =>
                {
                    if(failChain == null)
                    {
                        deferred.Reject(exception1);
                    }
                    else
                    {
                        failChain(exception1).Then(deferred.Resolve, deferred.Reject);
                    }
                });
            });
        }

        public IDeferred<T> Promise()
        {
            return new ConstrainedDeferred<T>(this);
        }

        #endregion

        private void OnStateChanged()
        {
            switch(_state)
            {
                case DeferredState.Pending:
                    throw new InvalidOperationException("Cannot change the state of a deferred object back to pending.");
                case DeferredState.Resolved:
                    lock(_doneCallbacks)
                    {
                        InvokeCallbacks(_doneCallbacks, _result);
                    }
                    break;
                case DeferredState.Rejected:
                    lock(_failCallbacks)
                    {
                        InvokeCallbacks(_failCallbacks, _exception);
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private static void InvokeCallbacks<TArg>(IEnumerable<Action<TArg>> callbacks, TArg arg)
        {
            foreach(var callback in callbacks)
            {
                callback(arg);
            }
        }
    }

    public static class Deferred
    {
        public static IDeferred<T> New<T>(Action<IDeferred<T>> init)
        {
            var deferred = new Deferred<T>();
            init(deferred);
            return deferred;
        }
    }
}